package org.solrext.handler;

import org.apache.solr.handler.component.SearchHandler;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrQueryResponse;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.FieldType;
import org.apache.solr.analysis.TokenizerChain;
import org.apache.solr.analysis.TokenFilterFactory;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Token;

import java.io.StringReader;
import java.io.IOException;
import java.util.*;

/**
 * RequestHandler that provides much the same functionality as analysis.jsp.  Provides the ability to specify multiple
 * field types and field names in the same request.
 *
 * TODO (cmale) - Add query -> index value match highlighting
 * 
 * @author Chris Male
 */
public class TokenAnalysisRequestHandler extends SearchHandler {

    private static final String ANALYSIS_PARAM_PREFIX = "analysis";
    private static final String FIELD_NAME_PARAM = ANALYSIS_PARAM_PREFIX + ".fieldname";
    private static final String FIELD_TYPE_PARAM = ANALYSIS_PARAM_PREFIX + ".fieldtype";
    private static final String INDEX_VALUE_PARAM = ANALYSIS_PARAM_PREFIX + ".indexvalue";
    private static final String QUERY_VALUE_PARAM = ANALYSIS_PARAM_PREFIX + ".queryvalue";

    /**
     * {@inheritDoc}
     */
    @Override
    public void handleRequestBody(SolrQueryRequest solrQueryRequest, SolrQueryResponse solrQueryResponse) throws Exception {
        AnalysisRequest analysisRequest = resolveAnalysisRequest(solrQueryRequest.getParams());
        IndexSchema indexSchema = solrQueryRequest.getCore().getSchema();

        NamedList<NamedList> analysisResults = new NamedList<NamedList>();

        NamedList<NamedList> fieldTypeAnalysisResults = new NamedList<NamedList>();
        for (String fieldTypeStr : analysisRequest.getFieldTypes()) {
            FieldType fieldType = indexSchema.getFieldType(fieldTypeStr);
            fieldTypeAnalysisResults.add(fieldTypeStr, analyzeValues(analysisRequest, fieldType, null));
        }

        NamedList<NamedList> fieldNameAnalysisResults = new NamedList<NamedList>();
        for (String fieldName : analysisRequest.getFieldNames()) {
            FieldType fieldType = indexSchema.getField(fieldName).getType();
            fieldNameAnalysisResults.add(fieldName, analyzeValues(analysisRequest, fieldType, fieldName));
        }

        analysisResults.add("field_types", fieldTypeAnalysisResults);
        analysisResults.add("field_names", fieldNameAnalysisResults);
        solrQueryResponse.add("analysis", analysisResults);
    }

    // ================================================= Helper methods ================================================

    /**
     * Resolves the AnalysisRequest based on the parameters in the given SolrParams.
     * 
     * @param solrParams SolrParams taken from request
     * @return AnalysisRequest containing all the information about what needs to be analyzed, and using what fields/types
     */
    private AnalysisRequest resolveAnalysisRequest(SolrParams solrParams) {
        AnalysisRequest analysisRequest = new AnalysisRequest();

        if (solrParams.get(FIELD_TYPE_PARAM) != null) {
            analysisRequest.setFieldTypes(solrParams.get(FIELD_TYPE_PARAM).split(","));
        }
        if (solrParams.get(FIELD_NAME_PARAM) != null) {
            analysisRequest.setFieldNames(solrParams.get(FIELD_NAME_PARAM).split(","));
        }
        analysisRequest.setQueryValue(solrParams.get(QUERY_VALUE_PARAM));
        analysisRequest.setIndexValue(solrParams.get(INDEX_VALUE_PARAM));
        return analysisRequest;
    }

    /**
     * Analyzes the index value (if it exists) and the query value (if it exists) in the given AnalysisRequest, using
     * the Analyzers of the given field type.
     * 
     * @param analysisRequest AnalysisRequest from where the index and query values will be taken
     * @param fieldType Type of field whose analyzers will be used
     * @param fieldName Name of the field to be analyzed.  Can be {@code null}
     * @return NamedList containing the tokens produced by the analyzers of the given field, separated into an index and
     *                   a query group
     */
    private NamedList analyzeValues(AnalysisRequest analysisRequest, FieldType fieldType, String fieldName) {
        NamedList<NamedList> analyzeResults = new NamedList<NamedList>();
        if (analysisRequest.getIndexValue() != null) {
            NamedList analyzedTokens = analyzeValue(analysisRequest.getIndexValue(), fieldName, fieldType.getAnalyzer());
            analyzeResults.add("index", analyzedTokens);
        }
        if (analysisRequest.getQueryValue() != null) {
            NamedList analyzedTokens = analyzeValue(analysisRequest.getQueryValue(), fieldName, fieldType.getQueryAnalyzer());
            analyzeResults.add("query", analyzedTokens);
        }

        return analyzeResults;
    }

    /**
     * Analyzes the given value using the given Analyzer.
     * 
     * @param value Value to analyze
     * @param fieldName Name of the field to analyze. Maybe {@code null}
     * @param analyzer Analyzer to use
     * @return NamedList containing the tokens produced by analyzing the given value
     */
    private NamedList analyzeValue(String value, String fieldName, Analyzer analyzer) {
        if (TokenizerChain.class.isInstance(analyzer)) {
            return analyzeValue(value, (TokenizerChain) analyzer);
        }

        TokenStream tokenStream = analyzer.tokenStream(fieldName, new StringReader(value));
        NamedList namedList = new NamedList();
        namedList.add(tokenStream.getClass().getName(), convertTokensToNamedLists(analyze(tokenStream)));
        return namedList;
    }

    /**
     * Analyzes the given value using the given TokenizerChain.  This method differs from
     * {@link #analyzeValue(String, String, org.apache.lucene.analysis.Analyzer)} because the chain actually consists of
     * a tokenizer and a series of filters and the tokens produced by each, are collected. 
     * 
     * @param value Value to analyze
     * @param tokenizerChain TokenizerChain to use to analyze the value
     * @return NamedList containing the tokens produced by analyzing the value, separated by the tokenizer and filter
     *         class names.
     */
    private NamedList analyzeValue(String value, TokenizerChain tokenizerChain) {
        NamedList namedList = new NamedList();
        
        TokenStream tokenStream = tokenizerChain.getTokenizerFactory().create(new StringReader(value));
        List<Token> tokens = analyze(tokenStream);

        namedList.add(tokenStream.getClass().getName(), convertTokensToNamedLists(tokens));

        ListBasedTokenStream listBasedTokenStream = new ListBasedTokenStream(tokens);
        
        for (TokenFilterFactory tokenFilterFactory : tokenizerChain.getTokenFilterFactories()) {
            tokenStream = tokenFilterFactory.create(listBasedTokenStream);
            List<Token> tokenList = analyze(tokenStream);
            namedList.add(tokenStream.getClass().getName(), convertTokensToNamedLists(tokenList));
            listBasedTokenStream = new ListBasedTokenStream(tokenList);
        }

        return namedList;
    }

    /**
     * Analyzes the given TokenStream, collecting the Tokens it produces.
     * 
     * @param tokenStream TokenStream to analyze
     * @return List of tokens produced from the TokenStream
     */
    private List<Token> analyze(TokenStream tokenStream) {
        List<Token> tokens = new ArrayList<Token>();
        Token reusableToken = new Token();
        Token token = null;

        try {
            while ((token = tokenStream.next(reusableToken)) != null) {
                tokens.add((Token) token.clone());
            }
        } catch (IOException ioe) {
            throw new RuntimeException("Error occured while iterating over tokenstream", ioe);
        }

        return tokens;
    }

    /**
     * Converts the list of Tokens to a list of NamedLists representing the tokens.
     * 
     * @param tokens Tokens to convert
     * @return List of NamedLists containing the relevant information taken from the tokens
     */
    private List<NamedList> convertTokensToNamedLists(List<Token> tokens) {
        List<NamedList> tokensNamedLists = new ArrayList<NamedList>();

        Collections.sort(tokens, new Comparator<Token>() {
            public int compare(Token o1, Token o2) {
                return o1.endOffset() - o2.endOffset();
            }
        });

        int termPosition = 0;
        int termEnd = -1;

        for (Token token : tokens) {
            NamedList tokenNamedList = convertTokenToNamedList(token);
            if (token.endOffset() > termEnd) {
                tokenNamedList.add("position", ++termPosition);
                termEnd = token.endOffset();
            } else {
                tokenNamedList.add("position", termPosition);
            }
            tokensNamedLists.add(tokenNamedList);
        }

        return tokensNamedLists;
    }

    /**
     * Converts the given Token to a NamedList, containing all the tokens relevant information.
     * 
     * @param token Token to convert
     * @return NamedList containing the tokens relevant information
     */
    private NamedList convertTokenToNamedList(Token token) {
        NamedList tokenNamedList = new NamedList();

        tokenNamedList.add("text", token.term());
        tokenNamedList.add("type", token.type());
        tokenNamedList.add("start", token.startOffset());
        tokenNamedList.add("end", token.endOffset());
        if (token.getPayload() != null) {
            tokenNamedList.add("payload", token.getPayload());
        }

        return tokenNamedList;
    }

    // ================================================= Inner classes ================================================= 

    /**
     * TokenStream that iterates over a list of pre-existing Tokens
     */
    private static class ListBasedTokenStream extends TokenStream {
        private final Iterator<Token> tokenIterator;

        /**
         * Creates a new ListBasedTokenStream which uses the given tokens as its token source.
         *
         * @param tokens Source of tokens to be used
         */
        ListBasedTokenStream(List<Token> tokens) {
            tokenIterator = tokens.iterator();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Token next(Token token) throws IOException {
            return (tokenIterator.hasNext()) ? tokenIterator.next() : null;
        }
    }

    /**
     * Holds the values of the parameters related to the analysis to be executed
     */
    private static class AnalysisRequest {

        private String[] fieldNames = new String[0];
        private String[] fieldTypes = new String[0];

        private String indexValue;
        private String queryValue;

        public String[] getFieldNames() {
            return fieldNames;
        }

        public void setFieldNames(String[] fieldNames) {
            this.fieldNames = fieldNames;
        }

        public String[] getFieldTypes() {
            return fieldTypes;
        }

        public void setFieldTypes(String[] fieldTypes) {
            this.fieldTypes = fieldTypes;
        }

        public String getIndexValue() {
            return indexValue;
        }

        public void setIndexValue(String indexValue) {
            this.indexValue = indexValue;
        }

        public String getQueryValue() {
            return queryValue;
        }

        public void setQueryValue(String queryValue) {
            this.queryValue = queryValue;
        }
    }
}
